/*******************************************************************************
 * Copyright (c) 2012-2015 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.jdt.core.launching.environments;

import org.eclipse.che.jdt.core.launching.IVMInstallType;
import org.eclipse.che.jdt.core.launching.LibraryLocation;

import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.IAccessRule;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * A contributed execution environment.
 * 
 * @since 3.2
 */
public class ExecutionEnvironment implements IExecutionEnvironment {

//	/**
//	 * Add a VM changed listener to clear cached values when a VM changes or is removed
//	 */
//	private IVMInstallChangedListener fListener = new IVMInstallChangedListener() {
//
//		/* (non-Javadoc)
//		 * @see org.eclipse.jdt.launching.IVMInstallChangedListener#defaultVMInstallChanged(org.eclipse.jdt.launching.IVMInstall, org
//		 * .eclipse.jdt.launching.IVMInstall)
//		 */
//		public void defaultVMInstallChanged(IVMInstall previous, IVMInstall current) {
//		}
//
//		/* (non-Javadoc)
//		 * @see org.eclipse.jdt.launching.IVMInstallChangedListener#vmAdded(org.eclipse.jdt.launching.IVMInstall)
//		 */
//		public void vmAdded(IVMInstall newVm) {
//		}
//
//		/* (non-Javadoc)
//		 * @see org.eclipse.jdt.launching.IVMInstallChangedListener#vmChanged(org.eclipse.jdt.launching.PropertyChangeEvent)
//		 */
//		public void vmChanged(PropertyChangeEvent event) {
//			if (event.getSource() != null) {
//				fParticipantMap.remove(event.getSource());
//				fRuleCache.remove(event.getSource());
//			}
//		}
//
//		/* (non-Javadoc)
//		 * @see org.eclipse.jdt.launching.IVMInstallChangedListener#vmRemoved(org.eclipse.jdt.launching.IVMInstall)
//		 */
//		public void vmRemoved(IVMInstall removedVm) {
//			fParticipantMap.remove(removedVm);
//			fRuleCache.remove(removedVm);
//		}
//	};
//
//
//	/**
//	 * The backing <code>IConfigurationElement</code>
//	 */
//	private IConfigurationElement fElement;

	/**
	 * Environment specific rule participant or <code>null</code> if none.
	 */
	private IAccessRuleParticipant fRuleParticipant;

	/**
	 * OSGi profile properties or <code>null</code> if none.
	 */
	private Properties fProfileProperties;

	/**
	 * Whether profile properties have been initialized
	 */
	private boolean fPropertiesInitialized;

//	/**
//	 * Set of compatible vm's - just the strictly compatible ones
//	 */
//	private Set<IVMInstall> fStrictlyCompatible = new HashSet<IVMInstall>();
//
//	/**
//	 * All compatible vm's
//	 */
//	private List<IVMInstall> fCompatibleVMs = new ArrayList<IVMInstall>();

	/**
	 * default VM install or <code>null</code> if none
	 */
	private IVMInstallType fDefault = null;

	/**
	 * Cache of access rule participants to consider for this environment.
	 */
	private IAccessRuleParticipant[] fParticipants = null;

	/**
	 * Map of {IVMInstall -> Map of {participant -> IAccessRule[][]}}.
	 * Caches access rules returned by each participant for a given VM.
	 * @since 3.3
	 */
	private Map<IVMInstallType, Map<IAccessRuleParticipant, IAccessRule[][]>> fParticipantMap =
			new HashMap<IVMInstallType, Map<IAccessRuleParticipant, IAccessRule[][]>>();

	/**
	 * Cache of VM -> IAccessRule[][] based on the current state of the participant
	 * map. These are the union of the latest rules generated by the participants
	 * for a specific VM. 
	 * @since 3.3
	 */
	private Map<IVMInstallType, IAccessRule[][]> fRuleCache = new HashMap<IVMInstallType, IAccessRule[][]>();

	/**
	 * Wild card pattern matching all files
	 */
	private static final IPath ALL_PATTERN = new Path("**/*"); //$NON-NLS-1$

	/**
	 * Prefix of compiler settings in properties file
	 */
	private static final String COMPILER_SETTING_PREFIX = JavaCore.PLUGIN_ID + ".compiler"; //$NON-NLS-1$
	private              String id                      = "JavaSE-1.8";

	/**
	 * Constructor
	 * @param element the backing {@link org.eclipse.core.runtime.IConfigurationElement}
	 */
	public ExecutionEnvironment() {
		fPropertiesInitialized = false;
//		String attribute = fElement.getAttribute(EnvironmentsManager.RULE_PARTICIPANT_ELEMENT);
//		if (attribute != null) {
		fRuleParticipant = new DefaultAccessRuleParticipant();
//		}
//		JavaRuntime.addVMInstallChangedListener(fListener);
	}

	/**
	 * Initializes the <code>EnvironmentsManager</code>
	 */
	private void init() {
//		EnvironmentsManager.getDefault().initializeCompatibilities();
	}

	/* (non-Javadoc)
	 * @see org.eclipse.jdt.launching.environments.IExecutionEnvironment#getId()
	 */
	public String getId() {
//		return fElement.getAttribute("id"); //$NON-NLS-1$
//		throw new UnsupportedOperationException();
		return id;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.jdt.launching.environments.IExecutionEnvironment#getDescription()
	 */
	public String getDescription() {
//		return fElement.getAttribute("description"); //$NON-NLS-1$
		throw new UnsupportedOperationException();
	}

//	/* (non-Javadoc)
//	 * @see org.eclipse.jdt.launching.environments.IExecutionEnvironment#getCompatibleVMs()
//	 */
//	public IVMInstallType[] getCompatibleVMs() {
//		init();
//		return fCompatibleVMs.toArray(new IVMInstall[fCompatibleVMs.size()]);
//	}
//
//	/* (non-Javadoc)
//	 * @see org.eclipse.jdt.launching.environments.IExecutionEnvironment#isStrictlyCompatible(org.eclipse.jdt.launching.IVMInstall)
//	 */
//	public boolean isStrictlyCompatible(IVMInstall vm) {
//		init();
//		return fStrictlyCompatible.contains(vm);
//	}
//
//	/* (non-Javadoc)
//	 * @see org.eclipse.jdt.launching.environments.IExecutionEnvironment#getDefaultVM()
//	 */
//	public IVMInstall getDefaultVM() {
//		init();
//		return fDefault;
//	}
//
//	/* (non-Javadoc)
//	 * @see org.eclipse.jdt.launching.environments.IExecutionEnvironment#setDefaultVM(org.eclipse.jdt.launching.IVMInstall)
//	 */
//	public void setDefaultVM(IVMInstall vm) {
//		init();
//		if (vm != null && !fCompatibleVMs.contains(vm)) {
//			throw new IllegalArgumentException(NLS.bind(EnvironmentMessages.EnvironmentsManager_0, new String[]{getId()}));
//		}
//		if (vm != null && vm.equals(fDefault)) {
//			return;
//		}
//		fDefault = vm;
//		EnvironmentsManager.getDefault().updateDefaultVMs();
//		// update classpath containers
//		rebindClasspathContainers();
//	}

//	/**
//	 * Updates Java projects referencing this environment, if any.
//	 */
//	private void rebindClasspathContainers() {
//		IJavaModel model = JavaCore.create(ResourcesPlugin.getWorkspace().getRoot());
//		if (model != null) {
//			try {
//				List<IJavaProject> updates = new ArrayList<IJavaProject>();
//				IJavaProject[] javaProjects = model.getJavaProjects();
//				IPath path = JavaRuntime.newJREContainerPath(this);
//				for (int i = 0; i < javaProjects.length; i++) {
//					IJavaProject project = javaProjects[i];
//					IClasspathEntry[] rawClasspath = project.getRawClasspath();
//					for (int j = 0; j < rawClasspath.length; j++) {
//						IClasspathEntry entry = rawClasspath[j];
//						if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
//							if (entry.getPath().equals(path)) {
//								updates.add(project);
//							}
//						}
//					}
//				}
//				if (!updates.isEmpty()) {
//					JavaCore.setClasspathContainer(path,
//												   updates.toArray(new IJavaProject[updates.size()]),
//												   new IClasspathContainer[updates.size()],
//												   new NullProgressMonitor());
//				}
//			} catch (JavaModelException e) {
//				LaunchingPlugin.log(e);
//			}
//		}
//	}

//	/**
//	 * Adds the specified VM to the listing of compatible VMs, also
//	 * adds the VM to the listing of strictly compatible ones based on
//	 * the strictlyCompatible flag
//	 * @param vm the VM to add to the environment
//	 * @param strictlyCompatible if it is strictly compatible
//	 */
//	void add(IVMInstall vm, boolean strictlyCompatible) {
//		if (fCompatibleVMs.contains(vm)) {
//			return;
//		}
//		fCompatibleVMs.add(vm);
//		if (strictlyCompatible) {
//			fStrictlyCompatible.add(vm);
//		}
//	}
//
//	/**
//	 * Removes the specified VM from the listings of VMs
//	 * @param vm the VM to remove
//	 */
//	void remove(IVMInstall vm) {
//		fCompatibleVMs.remove(vm);
//		fStrictlyCompatible.remove(vm);
//	}

	/**
	 * Sets the default VM to be the one specified
	 * @param vm the VM to set as the default
	 */
	void initDefaultVM(IVMInstallType vm) {
		fDefault = vm;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.jdt.launching.environments.IExecutionEnvironment#getAccessRules(org.eclipse.jdt.launching.IVMInstall, org.eclipse.jdt.launching.LibraryLocation[], org.eclipse.jdt.core.IJavaProject)
	 */
	public IAccessRule[][] getAccessRules(IVMInstallType vm, LibraryLocation[] libraries, IJavaProject project) {
		IAccessRuleParticipant[] participants = getParticipants();
		Map<IAccessRuleParticipant, IAccessRule[][]> rulesByParticipant = collectRulesByParticipant(participants, vm, libraries, project);
		synchronized (this) {
			Map<IAccessRuleParticipant, IAccessRule[][]> cachedRules = fParticipantMap.get(vm);
			if (cachedRules == null || !cachedRules.equals(rulesByParticipant)) {
				ArrayList<List<IAccessRule>> libLists = new ArrayList<List<IAccessRule>>(); // array of lists of access rules
				for (int i = 0; i < libraries.length; i++) {
					libLists.add(new ArrayList<IAccessRule>());
				}
				for (int i = 0; i < participants.length; i++) {
					IAccessRuleParticipant participant = participants[i];
					addRules(rulesByParticipant.get(participant), libLists);
				}
				IAccessRule[][] allRules = new IAccessRule[libraries.length][];
				for (int i = 0; i < libLists.size(); i++) {
					List<IAccessRule> l = libLists.get(i);
					allRules[i] = l.toArray(new IAccessRule[l.size()]);
				}
				fParticipantMap.put(vm, rulesByParticipant);
				fRuleCache.put(vm, allRules);
				return allRules;
			}
			return fRuleCache.get(vm);
		}
	}

	/**
	 * Returns all access rule participants to consider for this environment.
	 * Includes any participant contributed with this environment and all other
	 * stand alone participants.
	 *
	 * @return access rule participants to consider for this environment
	 */
	private synchronized IAccessRuleParticipant[] getParticipants() {
		if (fParticipants == null) {
			// check participants first
			IAccessRuleParticipant[] participants = null;// EnvironmentsManager.getDefault().getAccessRuleParticipants();
			if (fRuleParticipant != null) {
				// ensure environment specific provider is last and not duplicated
				LinkedHashSet<IAccessRuleParticipant> set = new LinkedHashSet<IAccessRuleParticipant>();
//				for (int i = 0; i < participants.length; i++) {
//					set.add(participants[i]);
//				}
				// remove, add to make last
				set.remove(fRuleParticipant);
				set.add(fRuleParticipant);
				participants = set.toArray(new IAccessRuleParticipant[set.size()]);
			}
			fParticipants = participants;
		}
		return fParticipants;
	}

	/**
	 * Returns a map of participant to the access rules for that participant for the given
	 * VM, libraries, and project.
	 *
	 * @param participants access rule participants
	 * @param vm the VM
	 * @param libraries the {@link LibraryLocation}s
	 * @param project the {@link org.eclipse.jdt.core.IJavaProject} context
	 * @return the mapping of {@link IAccessRuleParticipant} to {@link org.eclipse.jdt.core.IAccessRule}s
	 */
	private Map<IAccessRuleParticipant, IAccessRule[][]> collectRulesByParticipant(IAccessRuleParticipant[] participants, IVMInstallType vm, LibraryLocation[] libraries, IJavaProject project) {
		Map<IAccessRuleParticipant, IAccessRule[][]> map = new HashMap<IAccessRuleParticipant, IAccessRule[][]>();
		for (int i = 0; i < participants.length; i++) {
			// TODO: use safe runnable
			map.put(participants[i], participants[i].getAccessRules(this, vm, libraries, project));
		}
		return map;
	}

	/**
	 * Adds the access rules to each list in the given collection. If the last rule in a
	 * given collection is the wild card pattern then no more rules are added to that collection.
	 *
	 * @param accessRules the list of {@link org.eclipse.jdt.core.IAccessRule}s
	 * @param collect the array of lists to collect the {@link org.eclipse.jdt.core.IAccessRule}s in
	 */
	private void addRules(IAccessRule[][] accessRules, ArrayList<List<IAccessRule>> collect) {
		for (int i = 0; i < accessRules.length; i++) {
			IAccessRule[] libRules = accessRules[i];
			List<IAccessRule> list = collect.get(i);
			// if the last rule is a **/* pattern, don't add any more rules, as they will have no effect
			if (!list.isEmpty()) {
				IAccessRule lastRule = list.get(list.size() - 1);
				if(lastRule.getPattern().equals(ALL_PATTERN)) {
					continue;
				}
			}
			for (int j = 0; j < libRules.length; j++) {
				list.add(libRules[j]);
			}
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.jdt.launching.environments.IExecutionEnvironment#getProfileProperties()
	 */
	public Properties getProfileProperties() {
		if (!fPropertiesInitialized) {
			fPropertiesInitialized = true;
//			String path = fElement.getAttribute("profileProperties"); //$NON-NLS-1$
//			Bundle bundle = null;
//			if (path == null) {
//				// attempt default profiles known to OSGi
//				bundle = Platform.getBundle("org.eclipse.osgi"); //$NON-NLS-1$
//				path = getId().replace('/', '_') + ".profile"; //$NON-NLS-1$
//			} else {
//				// read provided file
//				bundle = Platform.getBundle(fElement.getContributor().getName());
//			}
//			if (bundle != null && path != null) {
				fProfileProperties = getJavaProfileProperties();
//			}
		}
		return fProfileProperties;
	}
	
	/**
	 * Returns properties file contained in the specified bundle at the given
	 * bundle relative path, or <code>null</code> if none.
	 * 
	 * @param bundle bundle to locate file in
	 * @param path bundle relative path to properties file
	 * @return properties or <code>null</code> if none
	 */
	private Properties getJavaProfileProperties() {
		URL profileURL = getClass().getResource("/" + id + ".profile"); //bundle.getEntry(path);
		if (profileURL != null) {
			InputStream is = null;
			try {
//				profileURL = FileLocator.resolve(profileURL);
				is = profileURL.openStream();
				if (is != null) {
					Properties profile = new Properties();
					profile.load(is);
					return profile;
				}
			} catch (IOException e) {
			} finally {
				try {
					if (is != null) {
						is.close();
					}
				} catch (IOException e) {
				}				
			}
		}
		return null;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.jdt.launching.environments.IExecutionEnvironment#getSubEnvironments()
	 */
	public IExecutionEnvironment[] getSubEnvironments() {
//		Properties properties = getProfileProperties();
//		Set<IExecutionEnvironment> subenv = new LinkedHashSet<IExecutionEnvironment>();
//		if (properties != null) {
//			@SuppressWarnings("deprecation")
//			String subsets = properties.getProperty(Constants.FRAMEWORK_EXECUTIONENVIRONMENT);
//			if (subsets != null) {
//				String[] ids = subsets.split(","); //$NON-NLS-1$
//				for (int i = 0; i < ids.length; i++) {
//					IExecutionEnvironment sub = JavaRuntime.getExecutionEnvironmentsManager().getEnvironment(ids[i].trim());
//					if (sub != null && !sub.getId().equals(getId())) {
//						subenv.add(sub);
//					}
//				}
//			}
//		}
//		return subenv.toArray(new IExecutionEnvironment[subenv.size()]);
		throw new UnsupportedOperationException();
	}

	/* (non-Javadoc)
	 * @see org.eclipse.jdt.launching.environments.IExecutionEnvironment#getComplianceOptions()
	 */
	public Map<String, String> getComplianceOptions() {
		Properties properties = getProfileProperties();
		if (properties != null) {
			Map<String, String> map = new HashMap<String, String>();
			Iterator<?> iterator = properties.keySet().iterator();
			while (iterator.hasNext()) {
				String key = (String) iterator.next();
				if (key.startsWith(COMPILER_SETTING_PREFIX)) {
					map.put(key, properties.getProperty(key));
				}
			}
			if (!map.isEmpty()) {
				return map;
			}
		}
		return null;
	}	
}
